查看原文
其他

自定义View - 仿酷狗音乐主页面侧滑效果

Delusion 技术最TOP 2022-08-26

作者:Delusion链接:https://juejin.cn/post/7085922621761519623

概述

最近在用酷狗音乐时发现酷狗音乐的主页侧滑效果不错,忍不住手痒痒,就想实现一下看看

效果分析

  • 我们可以看出分为两个部分 (菜单页面、主题页面)所以这里采用自定义ViewGroup
  • 向左滑动时会有一个放大和缩小的动画,在这里基本可以确定需要处理触摸事件,及触摸时的动画
  • 快速滑动时的打开关闭处理(GestureDetector收拾处理类)

实现思路

这里我们选择继承HorizontalScrollView 水平滚动布局 来实现,然后写好布局文件这个和HorizontalScrollView的用法一样,初时进来滚动到只有主页面的地方,在onTouch中根据滑动的距离来控制布局的缩放动画

实现

public KGSlidingMenu(Context context) {
    this(context, null);
}

public KGSlidingMenu(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public KGSlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

运行起来之后发现布局不对,完全乱了,这里我们在代码里设置菜单页面、主题页面的宽度

  • 菜单页面的宽度 这里我们定义一个属性 menu_width
  • 主题页面的宽度 = 屏幕的宽度
private View mMenuView;
private View mContainerView;
private int mMenuWidth;//需要自定义属性获取



public KGSlidingMenu(Context context) {
    this(context, null);
}

public KGSlidingMenu(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public KGSlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.KGSlidingMenu);
    mMenuWidth = Math.round(ta.getDimension(R.styleable.KGSlidingMenu_menu_width, 325));
    ta.recycle();
 }
 
 
@Override
protected void onFinishInflate() {//在onCreate中执行
    super.onFinishInflate();
    //这个方法代表整个布局解析完毕
    //指定宽高 :内容页的宽度为屏幕的宽度、菜单页的宽度由使用者自己在xml中定义  app:menu_width=""
    //获取菜单和容器View
    //获取的是LinearLayout
    ViewGroup mRootView = (ViewGroup) getChildAt(0);
    if (mRootView == null) {
        throw new NullPointerException("child can not be null !!!");
    }
    if (mRootView.getChildCount() != 2) {
        throw new RuntimeException("Only two can be placed View !!!");
    }
    //获取的是菜单和容器的跟布局
    mMenuView = mRootView.getChildAt(0);
    mContainerView = mRootView.getChildAt(1);
    //给菜单布局设置指定宽度  给容器布局设置屏幕的宽高
    //只能通过layoutParams设置宽高
    mMenuView.getLayoutParams().width = mMenuWidth;
    mContainerView.getLayoutParams().width = getScreenWidth(mContext);
  
}

//获取屏幕宽度
public int getScreenWidth(Context mContext) {
    WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics displayMetrics = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(displayMetrics);
    return displayMetrics.widthPixels;
}

目前的效果就是可以滑动,并且菜单和主页面内容的布局宽度正常,现在存在的问题:

  • 在刚进来时,菜单是打开的,正常情况应该是关闭的
  • 在手指抬起的时候,应该根据当前位置去判断菜单时打开,还是关闭
  • 在滑动时 没有相应的动画


第一点 在刚进来时,菜单是打开的,这一点我们可以在布局摆放完成时,是我们KGSlidingMenu滚动到相应的位置 具体实现如下

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {//在onResume中执行
    super.onLayout(changed, l, t, r, b);
    //初始化进来 mMenuView 模式是不显示的
    //用来摆放子View,等所有子View  摆放完毕才能滚动
    scrollTo(mMenuWidth, 0);
}

第二点 在手指抬起的时候,应该根据当前位置去判断菜单时打开,还是关闭。判断菜单打开关闭的条件是我们滑动的距离和菜单的宽做对比

  • 1).在关闭时,向右滑动,如果滑动的距离小于菜单宽度的一半,这是当我们抬起手指时,应当关闭菜单,大于菜单宽度的一半时,应当打开菜单.
  • 2).在打开时,向左滑动,如果滑动的距离小于菜单宽度的一半,这是当我们抬起手指时,应当打开菜单,大于菜单宽度的一半时,应当关闭菜单.

因此我们需要在onTouchEvent()中处理相应的触摸滑动事件

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (mGestureDetector != null) {
        if (mGestureDetector.onTouchEvent(event)) return true//快速滑动执行了,就不要执行onTouch中手指抬起事件
    }
    if (onInterceptTouchEvent(event)) return true;

    //手指抬起
    if (MotionEvent.ACTION_UP == event.getAction()) {
        int mCurrentScrollX = getScrollX();
        float mCurrentScrollY = getScrollY(); //避免竖向触摸时触发事件
        if (Math.abs(mCurrentScrollX) > Math.abs(mCurrentScrollY) * 2 / 3) {
            if (mCurrentScrollX < mMenuWidth / 2) {//未滚动到mMenuView宽度的一半
                //打开菜单
                openMenu();
            } else {
                //关闭菜单
                closeMenu();
            }
        }

        return false;//确保 super.onTouchEvent(event)  不会执行
    }

    return super.onTouchEvent(event);
}
private void closeMenu() {
    smoothScrollTo(mMenuWidth, 0)
}


private void openMenu() {
    smoothScrollTo(00);
}

第三点 在滑动时 没有相应的动画  通过效果我们可以看到

  • 1). 右边的动画效果为缩放效果
  • 2). 左边的动画效果有渐变、缩放和位移

这些动画执行的进度和效果都是根据滑动的距离来决定时,因此我们需要获得滑动的距离

//滚动回调  处理右边View的缩放  左边View的缩放和透明度
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
   super.onScrollChanged(l, t, oldl, oldt);
   //l:left 变化mMenuWidth -> 0
   //算一个梯度值
   float scale = 1f * l / mMenuWidth; //从1->0
  
   //计算右边的缩放值
   float rightScaleValue = 0.85f + 0.15f * scale;
   //设置右边的缩放  默认以view的中心点缩放
   ViewCompat.setPivotX(mContainerView, 0);
   ViewCompat.setPivotY(mContainerView, mContainerView.getMeasuredHeight() / 2);
   ViewCompat.setScaleX(mContainerView, rightScaleValue);
   ViewCompat.setScaleY(mContainerView, rightScaleValue);

   //设置右边的菜单的透明度  由半透明到全透明 0。85-1
   //缩放 0.85-1

   //透明度
   float leftAlphaValue = (1 - scale) * 0.5f + 0.5f;
   ViewCompat.setAlpha(mMenuView, leftAlphaValue);

   float leftScaleValue = (1 - scale) * 0.15f + 0.85f;
   ViewCompat.setScaleX(mMenuView, leftScaleValue);
   ViewCompat.setScaleY(mMenuView, leftScaleValue);

   //刚开始退出是在右边而不是左边
   //设置平移
   // ViewCompat.setTranslationX(mMenuView,l);抽屉效果
   ViewCompat.setTranslationX(mMenuView, 0.2f * l);
   }

到此我们可以看到效果基本成型了,下面处理一下细节方面的优化

处理一下在手指快速滑动的是效果,如果我们的触摸事件响应了快速滑动的话,就不要执行onTouchEvent中的相关操作,否则会出现效果不对的冲突事件,因此我们需要在onTouchEvent中添加一下这段代码

if (mGestureDetector != null) {
    if (mGestureDetector.onTouchEvent(event)) return true//快速滑动执行了,就不要执行onTouch中手指抬起事件
}
复制代码
mGestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() {

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {//快速滑动
        //只要快速滑动就会回调
        //打开的时候往右快速滑动,就去关闭  关闭的时候往左边快速滑动就去打开
        //快速往右边滑动是一个正数  快速往左边滑动是一个负数
        if (Math.abs(velocityX) < Math.abs(velocityY) * 2 / 3return false;
        if (isMenuOpen) {
            //打开的时候往右快速滑动,就去关闭
            if (velocityX < 0) {
                closeMenu();
                return true;
            }
        } else {
            //关闭的时候往左边快速滑动就去打开
            if (velocityX > 0) {
                openMenu();
                return true;
            }
        }
        return false;
    }
});

注意查看的话可以发现当菜单打开时,我们们点击菜单的右边主题部分,不会响应主题页面的相关事件,只会关闭菜单,只有在菜单关闭时,才会触发右边主题部分的相关事件,因此我们这地方需要在拦截事件onInterceptTouchEvent中判断菜单是否关闭,进行事件的拦截,因此我们需要在上面打开关闭菜单的方法里添加一个标识符isMenuOpen来区分菜单是否打开,这里事件拦截之后我们直接关闭了菜单,所以不需要执行onTouchEvent中手指抬起的相关操作。

if (onInterceptTouchEvent(event)) return true;
复制代码
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (isMenuOpen && event.getX() > mMenuView.getWidth() && event.getAction() == MotionEvent.ACTION_UP) {
        closeMenu();
        return true;
    }
    return false;
}

至此相关效果基本就完成了,我们来看一下吧


完工!



---END---


推荐阅读:
丝滑~Android自定义树状图控件!
Android自定义LayoutManager实现可滚动的环形菜单!
面经!B站Android面试小记
Android 组件化架构设计从原理到实战!
Android 12 自动适配 exported 深入解析避坑
已投入生产的Android动态切换应用图标方案
写给Android工程师的AOP知识!

更文不易,点个“在看”支持一下👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存